package neuralmusic.brain;

import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.concurrent.DelayQueue;

import javax.swing.Timer;

import uk.co.drpj.util.tweaks.Tweakable;
import uk.co.drpj.util.tweaks.TweakableDouble;



public class Module2DManualBuild extends Module2D {
	
	enum State {VIRGIN,INPUTS,GREY,OUTPUTS,INPUTCONNECT,DONE};
	State state=State.VIRGIN;
	
	private final TweakableDouble threshModOut;
	private final TweakableDouble threshModIn;
	private final TweakableDouble noiseLevel;
	private final TweakableDouble halfLife;
	TweakableDouble threshModGrey;

	private Timer noiseTimer;
	NeuronParams pramGrey;

	public static Module build2DBrain(int nIn, int nOut, int nInternal,
			int nInternalConnect, int nInToGrey, NeuronParams pramIn,
			NeuronParams pramGrey, NeuronParams pramOut, Float weightInToGrey,
			Float internalConnectWeight, Float weightGrayToOut, Rectangle rect)
			throws Exception {

		Module2DManualBuild brain=new Module2DManualBuild();
		
		int marg = 20;

		Point p1 = new Point(rect.x, rect.y);
		Point p2 = new Point(rect.x, rect.height);

		// Inputs

		if (pramIn == null) {
			pramIn = new NeuronParams();
			pramIn.thresh = 0.01f;
			pramIn.delayInSecs = 0L; // don't delay inputs
			pramIn.outputVal = 1.0f;
			pramIn.timeConstant = (float) TimeRef.halfLifeToTimeConstant(.2);
		}

		Float inConnectWeight = 1.0f;

		brain.createInputs(nIn, p1, p2, pramIn, inConnectWeight);

		// outputs
		Point p3 = new Point(rect.width, 0);
		Point p4 = new Point(rect.width, rect.height);

		if (pramOut == null) {
			pramOut = new NeuronParams();
			pramOut.thresh = 0.1f;
			pramOut.delayInSecs = 0; // don't delay outputs
			pramOut.outputVal = 1.0f;
			pramOut.timeConstant = (float) TimeRef.halfLifeToTimeConstant(10);
		}

		brain.addOutputs(nOut, p3, p4, pramOut);

		// Grey matter
		if (pramGrey == null) {
			pramGrey = new NeuronParams();

			pramGrey.delayInSecs = (float) .01;
			pramGrey.timeConstant = (float) (TimeRef
					.halfLifeToTimeConstant(2 * pramGrey.delayInSecs));
		}
		Rectangle rect2 = new Rectangle(rect.x + marg, rect.y + marg,
				rect.width - 3 * marg, rect.height - 2 * marg);

		// make this so it does not go unstable.
		if (internalConnectWeight == null)
			internalConnectWeight = (float) (100.0) / nInternalConnect;

		brain.addGreyMatter(nInternal, nInternalConnect, rect2,
				internalConnectWeight, pramGrey);

		// connect input to grey matter

		if (weightInToGrey == null)
			weightInToGrey = 1.0f;

		brain.connectInputsToGreyMatter(nInToGrey, weightInToGrey);

		if (weightGrayToOut == null)
			weightGrayToOut = 1.0f;

		brain.connectGreyMatterToOutputs(weightGrayToOut);

		return brain;

	}

	
	public Module2DManualBuild() {
	
		fireQueue = new DelayQueue<Neuron>();

		//inputs = new ArrayList<Connection>();

		threshModGrey = new TweakableDouble(tweaks, 0.0, 100.0, 1.0, 0.1,
				"Grey Threshold");
		
		threshModOut = new TweakableDouble(tweaks, 0.0, 100.0, 1.0, 0.1,
				"Out Threshold");
		
		threshModIn = new TweakableDouble(tweaks, 0.0, 100.0, 1.0, 0.1,
				"In Threshold");

		noiseLevel = new TweakableDouble(tweaks, 0.0, 1.0, 0.0, 0.01, "Noise");
		
		halfLife = new TweakableDouble(tweaks, 0.0, 1000.0, 1.0, 1.0,
				"Grey halflife [mS]");

		halfLife.addObserver(new Observer() {

			@Override
			public void update(Observable o, Object arg) {
				pramGrey.timeConstant = (float) TimeRef.halfLifeToTimeConstant(halfLife.doubleValue());
			}
		});
		

		noiseTimer = new Timer(10, new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				// TODO 
				int i = (int) (greyMatter.size() * rand.nextDouble());
				float noise = (float) noiseLevel.doubleValue();
				greyMatter.get(i).excite(noise);
			}
		});
	}
	
	// create input neurons
	// create the connections so we can fire them 
	public void createInputs(int size, Point p1, Point p2,
			NeuronParams pramIn,float inputConnectWeight) throws Exception {
		if (state != State.VIRGIN){
			throw new Exception(" Can only add inputs to a VIRGIN brain");
		}
		
		for (int i = 0; i < size; i++) {
			float fact1 = ((float) (i+0.5)) / (size);
			float fact2 = (1.0f - fact1);
			Point p = new Point(p1.x * fact1 + fact2 * p2.x, p1.y * fact1
					+ fact2 * p2.y);
			Neuron n = new Neuron(p, fireQueue, pramIn, threshModIn,this);
			Connection c = new Connection(n, inputConnectWeight,connections); 
		//	inputs.add(c);
			visible.add(n);
		}
		state=State.INPUTS;
		setInputSlice(0,size);
	}
	
	// Create output neurons
	// and connect them top the given actions
	public void addOutputs(int size, Point p1, Point p2,
			NeuronParams pramOut) throws Exception {

		if (state != State.INPUTS){
			throw new Exception(" PLease add outsputs after  INPUTS brain");
		}
		
		//int size = action.length;
		Neuron n;// = new Neuron[3];
		Point p; // = new Point[3];

		outStart=neurons.size();
		outEnd=outStart+size;

		List<Neuron> outputs=new ArrayList<Neuron>();
		
		for (int i = 0; i < size; i++) {
		
			float fact1 = (float) (((i+0.5) * 3.0) / (3.0 * size));
			float fact2 = (1.0f - fact1);
			p = new Point(p1.x * fact2 + fact1 * p2.x, p1.y * fact2 + fact1
					* p2.y);
			n = new Neuron(p, fireQueue, pramOut, threshModOut,this);

			visible.add(n);		
			
		}
	
		
		for (Neuron nn : greyMatter) {
			for (Neuron o : outputs) {
				float weight = (rand.nextFloat() - 0.5f) * 2.0f * weightMax;
				Connection c2 = new Connection(o, weight,connections); // , 40f, null);
				nn.addOut(c2);
			}
		}
		state=State.DONE;

	}

	
	
// Create grey matter and connect them randomly
	
	public void addGreyMatter(int nAdd, int nConnect, Rectangle rect,
			float internalConnectWeight, NeuronParams pramGrey) throws Exception {

		if (state != State.OUTPUTS){
			throw new Exception(" Can only add grey matter after adding INPUTS amd OUTPUTS");
		}
	
		// create nuerons
		this.pramGrey=pramGrey;
		double delayMax=pramGrey.delayInSecs;
		ArrayList<Neuron> newMatter = new ArrayList<Neuron>();
		
		for (int i = 0; i < nAdd; i++) {

			float xFact = rand.nextFloat();
			float yFact = rand.nextFloat();
			pramGrey.delayInSecs = (float) (1e-3+delayMax * rand.nextDouble());
			Point p = new Point(rect.x + rect.width * xFact, rect.y
					+ rect.height * yFact);
			Neuron n = new Neuron(p, fireQueue, pramGrey, threshModGrey,this);

			// neurons.add(n);
			
			greyMatter.add(n);
			newMatter.add(n);
			visible.add(n);
		}

		int cnt = 0;

		double maxDist = rect.width * rect.width + rect.height * rect.height;

		int size=greyMatter.size();
		
		while (cnt < nAdd * nConnect) {

			for (Neuron n1 : newMatter) {
				Neuron n2 = greyMatter.get((int) (rand.nextDouble() * size));
				if (n1 == n2) {
					continue;    //  WHY ??
				}

				double dx = n1.pt.x - n2.pt.x;
				double dy = n1.pt.y - n2.pt.y;
				double sep = dx * dx + dy * dy;
				
				
				if (sep / maxDist > rand.nextDouble()) {
					continue;
				}

				Neuron in,out;
				
					
				if (rand.nextDouble() < 0.5){
					in=n1;
					out=n2;
				} else {
					in=n2;
					out=n1;
					
				}
		
				float weight = (rand.nextFloat() - 0.5f) * 2
						* internalConnectWeight;
				Connection c = new Connection(out, weight,connections); 
				in.addOut(c);
				out.addIn(c);
				cnt++;
			}
		}
		state=State.GREY;
	}
	
	// connect the input neurons to the grey stuff
	public void connectInputsToGreyMatter(int nConnect, float weightMax) throws Exception {

		if (state != State.GREY){
			throw new Exception(" Can only add grey matter after adding OUTPUTS");
		}
		int size = greyMatter.size();
		
		ArrayList<Connection> inList=new ArrayList<Connection>(getInputs());
		
		for (Connection c : inList) {
			for (int i = 0; i < nConnect; i++) {
				Neuron out = greyMatter.get((int) (rand.nextDouble() * size));
				float weight = (rand.nextFloat() - 0.5f) * 2.0f * weightMax;
				Connection c2 = new Connection(out, weight,connections); // , weightMax,
																// null);
				((Neuron) (c.target)).addOut(c2);
			}
		}
		state=State.INPUTCONNECT;
	}
	
	
	// connect grey matter to the outputs 
	public void connectGreyMatterToOutputs(float weightMax) throws Exception {
		
		if (state != State.INPUTCONNECT){
			throw new Exception(" Can only add grey matter after adding CONNECTING INPUTS");
		}
			}


	
	public void start() throws Exception {
		if (state != State.DONE){
			throw new Exception(" Can only start when completely constrcuted");
		}
	
	}
	

//	public void createOutputConnections(int nOut) {
//
//		Point dx = new Point(6, 0);
//
//		
//		
//		int size = neurons.size();
//		int i = size - nOut; 
//		int ia = 0;
//		for (; i < size; i++, ia++) {
//			Neuron n = neurons.get(i);
//			Connection c = new Connection(null, 1.0f, connections);
//			n.addOut(c);
//		}
//}

}
